package com.ruoyi.project.download;


import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.StringTokenizer;

/**
 * Implementation for {@link DownloadRequestProcessor} which support file download and
 * <tt>Range</tt> download.
 *
 * @author zw
 */
@Slf4j
@Service
public class DefaultDownloadRequestProcessor implements DownloadRequestProcessor {

  private final FileBytesCopier fileBytesCopier;

  public DefaultDownloadRequestProcessor(FileBytesCopier fileBytesCopier) {
    this.fileBytesCopier = fileBytesCopier;
  }

  @Override
  public void process(String path, HttpServletRequest request, HttpServletResponse response) throws IOException {

    InnerProcessor processor = new InnerProcessor(path, request, response);
    processor.validate();
    if (processor.hasError) {
      return;
    }
    Path filePath = processor.filePath;
    File downloadFile = filePath.toFile();
    long fileLength = downloadFile.length();
    if (fileLength == 0) {
      return;
    }
    String contentType = Files.probeContentType(filePath);
    ServletOutputStream out = response.getOutputStream();
    String rangeHeader = request.getHeader("Range");
    if (fileLength > Integer.MAX_VALUE) {
      //超过2G文件，默认的方式
      try {
        DownloadReadFileUtils.readFileContent(request, response, downloadFile);
      } catch (Exception ex) {
        log.error("DownloadReadFileUtils.readFileContent error", ex);
      }
    } else {
      if (rangeHeader == null) {
        // Copy all of bytes of file to OutputStream
        final Range allRange = new Range(0, fileLength - 1, fileLength);
        response.setHeader("Content-disposition", "attachment; filename=" + filePath.getFileName());
        response.setContentLengthLong(fileLength);
        response.setContentType(contentType);
        doCopyRange(out, filePath, allRange);
      } else {
        // For Range request
        response.setHeader("Accept-Ranges", "bytes");
        processor.parseRanges(rangeHeader, fileLength);
        if (processor.hasError) {
          return;
        }

        Range range = processor.range;
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        response.setContentLengthLong(range.end - range.start + 1);
        response.addHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length);
        response.setContentType(contentType);
        doCopyRange(out, filePath, range);
      }
    }
  }

  private void doCopyRange(OutputStream out, Path filePath, Range range) {
    try {
      fileBytesCopier.copyRange(out, filePath, range);
    } catch (Exception e) {
      log.warn(e.getMessage());
    }
  }


  static class InnerProcessor {

    final HttpServletRequest request;
    final HttpServletResponse response;
    final String originPath;
    private String path;
    private Path filePath;
    boolean hasError;
    Range range;


    InnerProcessor(String path, HttpServletRequest request, HttpServletResponse response) {
      this.originPath = path;
      this.request = request;
      this.response = response;
    }

    void setError() {
      this.hasError = true;
    }

    void validate() throws IOException {

      decodePathAndSendErrorIfError();
      if (hasError) {
        return;
      }

      this.filePath = Paths.get(path);
      checkFile();
    }

    private void decodePathAndSendErrorIfError()
        throws IOException {
      try {
        this.path = decodePath(originPath);
      } catch (UnsupportedEncodingException e) {
        setError();
        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
      }
    }

    private String decodePath(String path) throws UnsupportedEncodingException {
      return URLDecoder.decode(path, StandardCharsets.UTF_8.name());
    }

    private void checkFile() throws IOException {

      if (!Files.exists(filePath) || Files.isDirectory(filePath)) {
        setError();
        response.sendError(HttpServletResponse.SC_NOT_FOUND, filePath.toString());
        return;
      }

      if (!Files.isReadable(filePath)) {
        setError();
        response.sendError(HttpServletResponse.SC_FORBIDDEN, filePath.toString());
      }
    }

    private void parseRanges(String rangeHeader, long fileLength) throws IOException {

      // bytes is the only range unit supported
      if (!rangeHeader.startsWith("bytes")) {
        response.addHeader("Content-Range", "bytes */" + fileLength);
        response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
        setError();
        return;
      }

      // Get after bytes=
      rangeHeader = rangeHeader.substring(6);

      // All of the ranges which are successfully parsed.
      ArrayList<Range> result = new ArrayList<>();
      StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");

      // Parsing the range list
      while (commaTokenizer.hasMoreTokens()) {
        String rangeDefinition = commaTokenizer.nextToken().trim();

        Range currentRange = new Range();
        currentRange.length = fileLength;

        int dashPos = rangeDefinition.indexOf('-');

        if (dashPos <= 0) {
          response.addHeader("Content-Range", "bytes */" + fileLength);
          response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
          return;
        } else {
          try {
            currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos));
            if (dashPos < rangeDefinition.length() - 1) {
              currentRange.end = Long
                  .parseLong(rangeDefinition.substring(dashPos + 1, rangeDefinition.length()));
            } else {
              currentRange.end = fileLength - 1;
            }
          } catch (NumberFormatException e) {
            response.addHeader("Content-Range", "bytes */" + fileLength);
            response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return;
          }

        }

        if (!currentRange.validate()) {
          response.addHeader("Content-Range", "bytes */" + fileLength);
          response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
          return;
        }

        result.add(currentRange);
      }

      // Fow now, we only support one range
      // May be add more in future
      if (result.size() > 1) {
        response.addHeader("Content-Range", "bytes */" + fileLength);
        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, "Un-support multiple range");
        return;
      }

      this.range = result.get(0);
    }
  }

}
